package grammarLL1;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class Grammar {
	protected static Map<String, NonTerminal> nonTerminals;
	protected static Map<String, Terminal> terminals;
	private List<Production> productions;
	private TableLL1 table;
	
	public static void main(String[] args) {
		Grammar g;
		String aLine = "";
		BufferedReader buf;
		
		String filename = null;
		if (args == null || args.length == 0 ) {
			filename = "resources/grammar.txt";
		} else {
			filename = args[0];
		}
		
		
		try {
			GrammarBuilder gb = new FileGrammarBuilder(filename);
			g = new Grammar(gb);
			buf = new BufferedReader(new InputStreamReader(System.in));
			
			System.out.println("Enter a word to parse, or type help to view available commands");
			while(!aLine.equals("quit")) {
				aLine = buf.readLine();
				if (aLine != null) {
					if (aLine.equals("help")) {
						showHelp();
					} else if (aLine.equals("quit")){
						System.out.println("Program terminated by user");
						
					} else {
						System.out.println(g.parseLL1(aLine));
					}
				} else {
					aLine = ""; // evita excepcion de NullPointer
				}
			}
		} catch (FileNotFoundException e) {
			System.out.println("Grammar file not found");
		} catch (GrammarException e) {
			e.getMessage();
		} catch (IOException e) {
			System.out.println("Error reading user input, the following message was returned: " + e.getMessage());
		}
	}
	
	private static void showHelp() {
		System.out.println("Help - " +
				"commands:\nquit, terminate program\n" +
				"help, shows this help\n" +
				"%s, returns true if %s belongs to" +
				" the grammar loaded from file");
	}

	public Grammar(GrammarBuilder gb) throws GrammarException {
		nonTerminals = new HashMap<String, NonTerminal>();
		terminals = new HashMap<String, Terminal>();
		terminals.put("!", new Terminal("!")); // agrega lambda
		productions = gb.buildProductions();
		table = new TableLL1();
		buildTable(); // arma la tabla LL1
	}
	
	public boolean parseLL1(String word) {
		LinkedList<Symbol> stack;
		boolean ans, finished;
		int i;
		// genera el stack para pushear contenido de las celdas de la tabla
		stack = new LinkedList<Symbol>();
		stack.push(nonTerminals.get(NonTerminal.initialSymbol)); // push simbolos inicial a la pila
		finished = false;
		ans = false;
		i = 0;
		
		if (word.length() == 0) {
			return false;
		}
		
		//TODO: modularizar
		while(!finished) {
			
			// si el stack se vacio, controla que tmb se haya leido toda la cadena
			if (stack.isEmpty()) {
				if (i < word.length()) {
					finished = true;
					ans = false;
				} else {
					finished = true;
					ans = true;
				}
			} else if (i == word.length()){
				finished = true;
				ans = false;
			}else {
				// si no se vacio el stack ni se termino la cadena...
				Symbol s = stack.pop();
				
				// si lo popeado es un no terminal...
				if (s instanceof NonTerminal) {
					List<Symbol> ls;
					String term = String.valueOf(word.charAt(i));
					
					// obtiene de la tabla la parte derecha de la produccion
					// tomada de la (fil,col) = (noTerminal, terminal)
					ls = this.table.get(s, terminals.get(term));
					
					// si la celda no estaba vacia...
					if (ls != null) {
						// si la celda no contiene un lambda...
						if (!ls.contains(terminals.get("!"))) {
							
							//revierte los simbolos de la parte derecha
							// para pushearlos en orden inverso
							Collections.reverse(ls);
							
							// los pushea todos
							for (Symbol sym : ls) {
								if (sym instanceof NonTerminal) {
									stack.push(nonTerminals.get(sym.toString()));
								} else {
									stack.push(terminals.get(sym.toString()));
								}
							}
							
							// restaura la lista
							Collections.reverse(ls);
						}
					} else {
						// si el casillero SI estaba vacio, y el no-terminal NO es anulable, error
						if (!((NonTerminal)s).isNullable()) {
							finished = true;
							ans = false;
						}
					}
				} else {
					// si es terminal... comparo contra la cadena, si es distinto, error
					// sino simplemente consumo 1 caracter de la cadena
					String character = String.valueOf(word.charAt(i++));
					if (!s.toString().equals(character)) {
						finished = true;
						ans = false;
					}
				}
			}
		}
		return ans;
	}
	
	// TODO: Upgrade para que calcule primeros de un simbolos,
	// y no asumir que es el primer caracter de la parte derecha
	public static Terminal getDirectorSymbols(Production p) {
		return getFirst(p);
	}
	
	public static List<Symbol> getFirsts(NonTerminal nt) {
		List<Symbol> ls = new LinkedList<Symbol>();
		
		for (Production p : nt.getProductions()) {
			ls.add(getFirst(p));
		}
		
		return ls;
	}
	
	// TODO: Upgrade para que calcule primeros de cualquier produccion
	// de una gramatica LL1, ahora son todas VN -> VTalpha o VN -> lambda
	public static Terminal getFirst(Production p) {
		Symbol s;
		
		s = p.getSymbols().get(0); // el primero es un VT o lambda siempre!
		
		return (Terminal)s; // como es un VT o lambda, no falla
	}
	
	private void buildTable() {
		// por cada produccion de la gramatica, obtiene los simbolos
		// directrices y va armando la tabla
		for (Production p : this.productions) {
			NonTerminal nt = p.getN();
			this.table.put(nt, getDirectorSymbols(p), p.getSymbols());
		}
	}
}
